Introduction
首先先簡介 Closure 的特性
example 01 :
1
2
3
4
5
6
7
8
9function test() {
var a = 10
function inner() {
console.log(a) // 10
}
inner()
}
test()由這項 function,試著改寫成 => 不要直接執行 inner ,而是把這整個 function 直接回傳,會變成:
1
2
3
4
5
6
7
8
9
10function test() {
var a = 10
function inner() {
console.log(a) // 仍為 10
}
return inner // 注意:並非 return inner()
}
var inner = test()
inner()這時因為
return inner的關係,使變數a也存在於 function inner 之中,所以可以將「在 function 之中 return 一個 function」作為Closure現象。一項重要的優點為,可將變數隱藏在 function 內部,不使外部存取到這項變數,也就無法被隨意變更,如以下的例子:
example 02 :
1
2
3
4
5
6
7var myWallet = 100
function deduct(n) {
myWallet -= (n > 10 ? 10 : n)
}
deduct(13) // 90
myWallet -= 999 // -909原本變數在 function 內部中特定條件下執行特定的事情,但仍能被外部存取且修改,若利用 closure 改寫,就能夠避免這項問題。
1
2
3
4
5
6
7
8
9
10
11
12function getWallet() {
var myWallet = 100
return {
deduct: function(n) {
myWallet -= (n > 10 ? 10 : n)
}
}
}
var wallet = getWallet()
wallet.deduct(13) // 90
myWallet -= 999 // Uncaught ReferenceError: my_balance is not defined上述例子出現錯誤的原因為,因為變數被隱藏在 function 內部,因此外部無法存取到,若需要修改需透過執行
deduct這項 function,達到隱藏資訊的目的,變數不會被隨意更改。example 03 :
另一項常見的例子1
2
3
4
5
6
7
8var arr = []
for (var i = 0; i < 4; i++) {
arr[i] = function() {
console.log(i) // 4
}
}
arr[0]()原因為當我們呼叫
arr[0]()時,程式會去尋找這詞變數i為何,但是這時是迴圈已經全部跑完跳出時產生的i,因為 function 本身沒有i這項變數,因此往作用域的外層尋找時,就是找到這項跑完迴圈的i,因此i為 4 。若要解決這項問題,可使用幾種方式:
IIFE(Immediately Invoked Function Expression )可以將一個 function 包起來並把
i立即傳給程式執行,因此迴圈每跑一圈就會立刻呼叫一個新的 function ,也就是新產生一個新的作用域。1
2
3
4
5
6
7
8
9
10var arr = []
for (var i = 0; i < 4; i++) {
arr[i] = (function(num) {
return function() {
console.log(num)
}
})(i)
}
arr[0]()let
使用 ES6 語法
1
2
3
4
5
6
7
8var arr = []
for (let i = 0; i < 4; i++) {
arr[i] = function() {
console.log(i) // 4
}
}
arr[0]()
從 ECMAScript 中探討 scope
10.1.4 Scope Chain and Identifier ResolutionEvery execution context has associated with it a scope chain. A scope chain is a list of objects that are searched when evaluating an Identifier. When control enters an execution context, a scope chain is created and populated with an initial set of objects, depending on the type of code.
每個 EC 都有屬於自己的 scope chain,當進入 EC 時 scope chain 就會被建立。
10.2 Entering An Execution Context10.2.3 Function CodeThe scope chain is initialised to contain the activation object followed by the objects in the scope chain stored in the [[Scope]] property of the Function object.
當進入 EC 時,scope chain 會被初始化為 activation object,並加上 function 的 [[Scope]] 屬性。
1
scope chain = AO + [[Scope]]
13.2 Creating Function ObjectsGiven an optional parameter list specified by FormalParameterList, a body specified by FunctionBody, and a scope chain specified by Scope, a Function object is constructed as follows:
…- Set the [[Scope]] property of F to a new scope chain (10.1.4) that contains the same objects as Scope.
當我們建立 function 時會設定的 [[Scope]] ,裡面內含 scope 。
探討 closure 行程過程及原理
依據上述方式一步一步拆解過程
example
1
2
3
4
5
6
7
8
9
10var v1 = 10
function test() {
var vTest = 20
function inner() {
console.log(v1, vTest) // 10, 20
}
return inner
}
var inner = test()
inner()進入 global EC
進入 global EC,並初始化 VO and scope chain。
1
2
3
4
5
6
7
8globalEC {
VO: {
v1: undefined,
inner: undefined,
test: func
},
scopeChain: globalEC.VO
}執行主程式
執行
var v1 = 10以及var inner = test()。1
2
3
4
5
6
7
8
9
10globalEC {
VO: {
v1: 10,
inner: undefined,
test: func
},
scopeChain: globalEC.VO
}
test.[[Scope]] = globalEC.scopeChain進入 test EC
進入 test EC,並初始化 AO and scope chain。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19testEC {
AO: {
arguments,
vTest: undefined,
inner: func
},
scopeChain: [testEC.AO, globalEC.VO]
}
globalEC {
VO: {
v1: 10,
inner: undefined,
test: func
},
scopeChain: globalEC.VO
}
test.[[Scope]] = globalEC.scopeChain
執行 test 程式
執行
var vTest = 20與return inner。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19testEC {
AO: {
arguments,
vTest: 20,
inner: func
},
scopeChain: [testEC.AO, globalEC.VO]
}
globalEC {
VO: {
v1: 10,
inner: func,
test: func
},
scopeChain: globalEC.VO
}
inner.[[Scope]] = testEC.scopeChain = [testEC.AO, globalEC.VO]執行
return inner理論上
return inner後,function test() 執行完畢後資源會被釋放,但是因為1
inner.[[Scope]] = testEC.scopeChain = [testEC.AO, globalEC.VO]
inner.[[Scope]] 之中還有需要使用到 testEC.AO 的部分,因此儘管 test 這項 function 執行結束了,但是
testEC.AO仍需要被存在記憶體中。進入 inner EC
進入 inner EC,並初始化 AO and scope chain。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26innerEC {
AO: {
arguments
},
scopeChain: [innerEC.AO, testEC.AO, globalEC.VO]
}
testEC {
AO: {
arguments,
vTest: 20,
inner: func
},
scopeChain: [testEC.AO, globalEC.VO]
}
globalEC {
VO: {
v1: 10,
inner: func,
test: func
},
scopeChain: globalEC.VO
}
inner.[[Scope]] = testEC.scopeChain = [testEC.AO, globalEC.VO]執行 inner
1
2
3
4
5
6
7
8
9
10var v1 = 10
function test() {
var vTest = 20
function inner() {
console.log(v1, vTest) // 10, 20
}
return inner
}
var inner = test()
inner()執行完畢
結論
透過上述的拆解流程可以得知,其實當我們在宣告 function 時,程式背後的 compiler 就已經在幫我們建立 EC 以及初始化 EO/AO 的資訊了,並且把 scope 設定到 [[Scope]] 之中,因此當我們在這段程式碼之中:
1
2
3
4
5
6
7
8
9function test () {
let a = 10
function inner () {
console.log(a)
}
return inner
}
var inner = test()
inner()
使用 return inner 時,就能夠把內部的 function inner 回傳,使後續動作可以藉著執行 inner() 進行。而這樣的形式,我們可以說 inner() 這項 function 是在一個 Closure
之中,因為它也就像是被一項外層的 function 包裹起來。
Comments